{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "62420bec",
   "metadata": {},
   "source": [
    "# Chat models\n",
    "在 LangChain 中，Chat models 是专门用于处理对话式交互的组件，与普通的 LLM（大语言模型）组件相比，它的核心特点是接受 “消息列表” 作为输入（而非单一字符串），并能区分不同角色的消息（如系统指令、用户输入、模型回复），非常适合构建多轮对话场景（如聊天机器人、智能助手）。\n",
    "\n",
    "下面结合 DeepSeek 模型，详细介绍 Chat models 的核心概念、用法及示例。\n",
    "\n",
    "## 一、Chat models 的核心概念\n",
    "1. **消息类型（Message Types）**\n",
    "    \n",
    "    LangChain 定义了多种消息类型，用于区分对话中不同角色的输入，核心包括：\n",
    "    * SystemMessage：系统指令，用于定义模型的角色、行为规则（如 “你是嵌入式专家，回答需简洁”），优先级最高。\n",
    "    * HumanMessage：用户输入的消息（如 “如何配置 STM32 的 GPIO？”）。\n",
    "    * AIMessage：模型的历史回复（用于多轮对话时传递上下文）。\n",
    "    * FunctionMessage：工具调用的返回结果（用于 Agent 场景，暂不展开）。\n",
    "2. **输入输出形式**\n",
    "    * 输入：由上述消息类型组成的列表（如 [SystemMessage, HumanMessage, AIMessage, ...]）。\n",
    "    * 输出：AIMessage 对象，包含模型的回复内容（content 属性）。\n",
    "3. **与普通 LLM 的区别**\n",
    "    * 普通 LLM 接受单一字符串输入（如 llm.invoke(\"你好\")），适合单轮任务。\n",
    "    * Chat models 接受消息列表输入（如 chat_model([HumanMessage(content=\"你好\")])），天然支持多轮对话的上下文管理。"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "99cfec50",
   "metadata": {},
   "source": [
    "## 二、基于 DeepSeek 的 Chat models 示例\n",
    "以 DeepSeek 的对话模型（deepseek-chat）为例，展示 Chat models 的核心用法\n",
    "\n",
    "需要安装的包有：langchain openai langchain_community langchain_openai"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a6c4ac70",
   "metadata": {},
   "source": [
    "### 基础用法：单轮对话\n",
    "初始化 ChatOpenAI 实例（LangChain 中通过 ChatOpenAI 兼容 DeepSeek 等遵循 OpenAI API 格式的模型），并发送单轮消息。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "1a0f6429",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.messages import SystemMessage, HumanMessage\n",
    "import os\n",
    "api_base = os.getenv(\"OPENAI_API_BASE\")\n",
    "api_key = os.getenv(\"OPENAI_API_KEY\")\n",
    "\n",
    "# 初始化 DeepSeek 模型（需配置 API 密钥）\n",
    "llm = ChatOpenAI(\n",
    "    model_name=\"deepseek-chat\",  # 或 deepseek-coder 用于代码生成\n",
    "    openai_api_base=api_base,\n",
    "    openai_api_key=api_key,\n",
    "    temperature=0.3  # 控制输出随机性（0 表示更严谨）\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "3ab12ff5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "模型回复： STM32的GPIO输入模式主要有以下4种：\n",
      "\n",
      "1. **浮空输入（GPIO_Mode_IN_FLOATING）**\n",
      "   - 引脚处于高阻态，电平完全由外部电路决定\n",
      "   - 适用于按键检测等外部信号明确的场景\n",
      "\n",
      "2. **上拉输入（GPIO_Mode_IPU）**\n",
      "   - 内部上拉电阻（约30-50kΩ）使能\n",
      "   - 默认高电平，外部接地时被拉低\n",
      "\n",
      "3. **下拉输入（GPIO_Mode_IPD）**\n",
      "   - 内部下拉电阻使能\n",
      "   - 默认低电平，外部接高电平时被拉高\n",
      "\n",
      "4. **模拟输入（GPIO_Mode_AIN）**\n",
      "   - 引脚连接到ADC/DAC模块\n",
      "   - 数字施密特触发器被禁用，直接传输模拟信号\n",
      "\n",
      "配置要点：\n",
      "- 通过GPIOx_CRL/CRH寄存器设置模式\n",
      "- 输入模式下输出驱动器被自动禁用\n",
      "- 读取IDR寄存器获取引脚电平状态\n",
      "\n",
      "典型应用：\n",
      "- 浮空：通信总线（需外部上拉）\n",
      "- 上拉/下拉：按键、开关检测\n",
      "- 模拟：传感器信号采集\n"
     ]
    }
   ],
   "source": [
    "# 定义消息列表（系统指令+用户输入）\n",
    "messages = [\n",
    "    SystemMessage(content=\"你是一名精通嵌入式开发的工程师，回答需简洁专业，聚焦技术细节。\"),\n",
    "    HumanMessage(content=\"STM32的GPIO有哪几种输入模式？\"),\n",
    "]\n",
    "\n",
    "# 调用模型生成回复\n",
    "response = llm.invoke(messages)\n",
    "print(\"模型回复：\", response.content)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "81f4b3f9",
   "metadata": {},
   "source": [
    "### 进阶用法：多轮对话\n",
    "通过维护消息列表的历史记录，实现多轮上下文关联（模型会记住之前的对话内容）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "94a77d9e",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "用户：STM32的SPI通信中，NSS引脚的作用是什么？\n",
      "模型：NSS（Slave Select）引脚在STM32 SPI通信中用于主从设备的选择控制，主要作用包括：\n",
      "\n",
      "1. **片选功能**：主机通过拉低NSS引脚电平选中特定从机，建立通信链路；通信结束后拉高电平断开连接。\n",
      "\n",
      "2. **模式支持**：\n",
      "   - 硬件模式（NSS引脚为硬件控制）：自动管理片选信号\n",
      "   - 软件模式（NSS软件管理）：通过程序代码控制GPIO模拟片选时序\n",
      "\n",
      "3. **多主机冲突防护**：当STM32作为从机时，检测到NSS为高电平时会自动禁用SPI输出，避免总线冲突。\n",
      "\n",
      "注意：部分STM32型号支持NSS脉冲模式，可在每次数据传输后自动产生NSS脉冲，简化通信流程。\n",
      "\n",
      "用户：它的硬件片选和软件片选有什么区别？\n",
      "模型：好的。STM32 SPI的硬件NSS和软件NSS主要有以下核心区别：\n",
      "\n",
      "---\n",
      "\n",
      "### 1. 控制方式\n",
      "- **硬件NSS (Hardware NSS)**：\n",
      "    - 由**SPI外设本身自动管理**NSS引脚的电平。\n",
      "    - 当MCU作为**主设备 (Master)** 时，一旦使能SPI（设置`SPE=1`），NSS引脚会自动输出低电平；禁用SPI时自动恢复高电平。\n",
      "    - 当MCU作为**从设备 (Slave)** 时，自动检测NSS引脚电平：仅当NSS为低电平时才响应主机通信。\n",
      "\n",
      "- **软件NSS (Software NSS)**：\n",
      "    - NSS引脚的功能被**禁用**，它可以用作一个普通的GPIO（通常是输出模式）。\n",
      "    - **片选信号完全由用户软件控制**。你需要在通信前手动拉低GPIO（选中从机），通信结束后再手动拉高（取消选中）。\n",
      "\n",
      "---\n",
      "\n",
      "### 2. 配置方法\n",
      "- **硬件NSS**：\n",
      "    - 在SPI控制寄存器中设置 `SSM=0` (Slave Select Management = 硬件模式)。\n",
      "    - 同时需要正确配置`SSOE` (Slave Select Output Enable) 等位。\n",
      "    - 引脚必须配置为**复用功能**，而非普通GPIO。\n",
      "\n",
      "- **软件NSS**：\n",
      "    - 设置 `SSM=1` (软件管理模式)。\n",
      "    - 通过设置寄存器中的`SSI` (Internal Slave Select) 位来提供片选信号（主要用于多主模式避免冲突，**不直接控制物理引脚**）。\n",
      "    - **实际物理片选**：需要**额外使用一个GPIO引脚**，并通过代码手动控制其电平。\n",
      "\n",
      "---\n",
      "\n",
      "### 3. 应用场景与优缺点\n",
      "| 特性 | 硬件NSS | 软件NSS |\n",
      "| :--- | :--- | :--- |\n",
      "| **便利性** | **高**，自动控制，节省CPU开销 | **低**，需手动编写片选代码 |\n",
      "| **灵活性** | **低**，时序固定 | **高**，可自定义片选时序和延时 |\n",
      "| **引脚占用** | 使用固定的**SPI NSS引脚** | 可使用**任意GPIO**作为片选引脚 |\n",
      "| **多从机支持**| 困难，通常1个NSS引脚只能控制1个从机 | **简单**，可用多个GPIO控制多个从机 |\n",
      "| **典型应用** | 单主单从、标准SPI通信 | 单主多从、需要特殊时序、兼容非标设备 |\n",
      "\n",
      "---\n",
      "\n",
      "### 总结\n",
      "- **硬件NSS**：图省事、标准应用、让硬件自动处理。\n",
      "- **软件NSS**：要灵活、控制多从机、需要自定义时序。\n",
      "\n",
      "在绝大多数单主多从的应用中，**软件NSS（即用普通GPIO作片选）是更常见和灵活的选择**，因为STM32通常只有一个硬件NSS引脚，无法直接管理多个从设备。\n"
     ]
    }
   ],
   "source": [
    "# 初始化消息列表（包含系统指令）\n",
    "messages = [\n",
    "    SystemMessage(content=\"你是嵌入式工程师，用简洁的语言解答硬件问题。\")\n",
    "]\n",
    "\n",
    "# 第一轮对话：用户提问\n",
    "user_input1 = \"STM32的SPI通信中，NSS引脚的作用是什么？\"\n",
    "messages.append(HumanMessage(content=user_input1))  # 添加用户消息\n",
    "response1 = llm.invoke(messages)\n",
    "messages.append(response1)  # 添加模型回复到历史\n",
    "print(f\"用户：{user_input1}\")\n",
    "print(f\"模型：{response1.content}\\n\")\n",
    "\n",
    "# 第二轮对话：基于上一轮的追问（模型会理解“它”指NSS引脚）\n",
    "user_input2 = \"它的硬件片选和软件片选有什么区别？\"\n",
    "messages.append(HumanMessage(content=user_input2))\n",
    "response2 = llm.invoke(messages)\n",
    "messages.append(response2)\n",
    "print(f\"用户：{user_input2}\")\n",
    "print(f\"模型：{response2.content}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "088a3124",
   "metadata": {},
   "source": [
    "### 流式输出：实时获取回复（适合 UI 交互）\n",
    "在实际应用中（如聊天界面），通常需要 “边生成边显示” 的流式输出，通过 stream() 方法实现。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "4e1566e8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "模型流式输出：\n",
      "好的，作为嵌入式工程师，以下是STM32 DMA传输的详细步骤，我将以最通用的流程进行分点说明，该流程适用于大多数STM32系列（如F1/F4/H7等）。\n",
      "\n",
      "### STM32 DMA传输详细步骤\n",
      "\n",
      "#### 1. 硬件与时钟配置\n",
      "- **使能DMA时钟**：在RCC（复位与时钟控制）寄存器中，启用对应DMA控制器的时钟（例如DMA1或DMA2）。\n",
      "- **配置外设时钟**：确保源设备（如ADC、SPI）或目标设备（如USART、存储器）的时钟已启用。\n",
      "\n",
      "#### 2. DMA通道选择与链接\n",
      "- **确定DMA请求映射**：根据数据源和目标（如内存到外设、内存到内存），查阅芯片参考手册，选择正确的DMA控制器和通道（或Stream，对于具有DMA2D的型号）。\n",
      "- **避免通道冲突**：确保所选通道未被其他外设占用。\n",
      "\n",
      "#### 3. 配置DMA传输参数（通过DMA寄存器）\n",
      "- **设置源地址（Source Address）**：指定数据传输的起始地址（例如存储器数组地址或外设数据寄存器地址）。\n",
      "- **设置目标地址（Destination Address）**：指定数据接收的地址（例如外设数据寄存器或存储器地址）。\n",
      "- **配置数据传输方向**：选择传输方向（如内存到外设、外设到内存、内存到内存）。\n",
      "- **设置数据大小（Data Size）**：\n",
      "  - **数据宽度（Data Width）**：定义单次传输的数据位宽（如8位、16位、32位）。\n",
      "  - **传输数据量（Number of Transfers）**：指定总传输次数（即数据项数量）。\n",
      "- **配置地址递增模式**：\n",
      "  - 源/目标地址是否在每次传输后自动递增（例如存储器地址通常递增，外设寄存器地址通常固定）。\n",
      "- **设置循环模式（Circular Mode）**：选择是否启用循环传输（用于连续数据流，如ADC采样）。\n",
      "- **配置优先级（Priority）**：如果有多个DMA请求，设置通道优先级（低、中、高、最高）。\n",
      "\n",
      "#### 4. 中断配置（可选）\n",
      "- **使能传输完成中断（TCIE）**：数据全部传输完成后触发中断。\n",
      "- **使能半传输中断（HTIE）**：传输完成一半时触发中断（用于双缓冲机制）。\n",
      "- **使能传输错误中断（TEIE）**：传输错误时触发中断。\n",
      "- **在NVIC中启用DMA中断**：设置中断优先级并启用对应DMA通道的中断。\n",
      "\n",
      "#### 5. 启动DMA传输\n",
      "- **使能DMA通道**：通过设置DMA_CCR寄存器的EN位启动传输。\n",
      "- **外设触发DMA请求**：配置外设（如USART、ADC）使其在需要数据传输时发出DMA请求（例如使能USART的DMA发送使能位）。\n",
      "\n",
      "#### 6. 传输过程监控\n",
      "- **查询标志位**：如果不使用中断，可通过查询DMA标志位（如传输完成标志TC）判断传输状态。\n",
      "- **中断处理**：如果启用中断，在中断服务程序（ISR）中处理传输完成或错误，并清除相应标志位。\n",
      "\n",
      "#### 7. 传输完成处理\n",
      "- **禁用DMA通道**：传输完成后，根据需要禁用DMA通道（清除EN位）。\n",
      "- **处理数据**：在内存到内存传输中，目标存储器数据已更新；在外设传输中，数据已发送或接收完成。\n",
      "\n",
      "#### 8. 错误处理\n",
      "- **检查传输错误标志**：如果发生错误（如地址对齐错误），读取状态寄存器并处理。\n",
      "- **重新初始化DMA**：必要时复位DMA配置并重新启动传输。\n",
      "\n",
      "### 补充说明\n",
      "- 对于内存到内存传输，通常无需外设请求，直接使能DMA即可启动。\n",
      "- 不同STM32系列（如F0/F1/F4/H7）的DMA寄存器名称可能略有差异（例如DMA_Stream或DMA_Channel），但核心步骤一致。\n",
      "- 使用HAL库或LL库时，上述步骤可通过API函数封装（如HAL_DMA_Start()），但底层原理相同。\n",
      "\n",
      "如果需要针对具体外设（如ADC、UART）或型号的示例代码，我可以进一步提供。"
     ]
    }
   ],
   "source": [
    "# 流式输出示例\n",
    "user_input = \"详细说明STM32的DMA传输步骤，分点列出。\"\n",
    "messages = [\n",
    "    SystemMessage(content=\"你是嵌入式工程师，回答需分点清晰。\"),\n",
    "    HumanMessage(content=user_input)\n",
    "]\n",
    "\n",
    "print(\"模型流式输出：\")\n",
    "# 调用 stream() 方法，逐块获取回复\n",
    "for chunk in llm.stream(messages):\n",
    "    print(chunk.content, end=\"\", flush=True)  # 实时打印，不换行"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a3592cbb",
   "metadata": {},
   "source": [
    "### 结合 ChatPromptTemplate：动态生成对话提示\n",
    "将 ChatPromptTemplate 与 Chat models 结合，实现动态生成带变量的对话提示（类似之前的 PromptTemplate，但专为对话场景设计）。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "a78b77be",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "SPI协议通过主从架构实现全双工同步通信，主设备控制时钟信号（SCLK）并选择从设备（CS），数据通过MOSI和MISO线在时钟边沿同步传输。\n"
     ]
    }
   ],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate, SystemMessagePromptTemplate, HumanMessagePromptTemplate\n",
    "\n",
    "# 1. 定义系统消息模板（带变量 {role}）\n",
    "system_template = \"你是{role}，回答需符合该角色的专业术语和风格。\"\n",
    "system_prompt = SystemMessagePromptTemplate.from_template(system_template)\n",
    "\n",
    "# 2. 定义用户消息模板（带变量 {question}）\n",
    "human_template = \"解释{question}的工作原理，用3句话以内说明。\"\n",
    "human_prompt = HumanMessagePromptTemplate.from_template(human_template)\n",
    "\n",
    "# 3. 组合成聊天提示模板\n",
    "chat_prompt = ChatPromptTemplate.from_messages([\n",
    "    system_prompt,\n",
    "    human_prompt\n",
    "])\n",
    "\n",
    "# 4. 填充变量（生成消息列表）\n",
    "messages = chat_prompt.format_prompt(\n",
    "    role=\"FPGA工程师\",\n",
    "    question=\"SPI协议\"\n",
    ").to_messages()\n",
    "\n",
    "# 5. 调用模型\n",
    "response = llm.invoke(messages)\n",
    "print(response.content)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5371d059",
   "metadata": {},
   "source": [
    "# LLMs\n",
    "在 LangChain v0.3 版本中，LLMs 组件（Large Language Models，大语言模型）专注于处理纯文本输入与输出，核心特点是接受单一字符串作为输入，返回单一字符串作为输出，不区分消息角色（如系统指令、用户输入），更适合单轮、无状态的文本生成任务（如代码生成、文本总结、翻译等）。\n",
    "\n",
    "与 Chat models 相比，LLMs 组件在接口设计和使用场景上有显著差异，下面结合 DeepSeek 模型（如 deepseek-coder 代码模型或 deepseek-chat 对话模型），详细介绍其核心用法、参数配置及适用场景（基于 LangChain v0.3 最新接口）。\n",
    "\n",
    "这个不详细介绍了，基本已过时。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
